Skip to content

Call the shared hygiene check beside this board's own gate (#178) - #179

Merged
iderex merged 2 commits into
mainfrom
issue-178-shared-hygiene
Aug 24, 2026
Merged

Call the shared hygiene check beside this board's own gate (#178)#179
iderex merged 2 commits into
mainfrom
issue-178-shared-hygiene

Conversation

@iderex

@iderex iderex commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The issue this belongs to

Closes #178

What changed

One file is added, .github/workflows/shared-hygiene.yml. It calls
iderex/wache/.github/workflows/pr-hygiene.yml, which holds the half of the
hygiene rules every board in this fleet has in common, with
subject_names_issue: false. Nothing else in the tree is touched:
.github/pr-hygiene/hygiene.sh and .github/workflows/pr-hygiene.yml keep
every rule they have, and the shared call runs beside them rather than in place
of them.

The pin is 113085b269d3437a3f96ff9e7060b64b0af88ab1, which is v1.2.0. It was
9b311243c2d0d0ced7feb957a20bc178acce6a5d, v1.0.0, and the section below is
why it moved.

Why this was red for a day, and what moved

This pull request was open and red on purpose from 2026-08-23 until the pin
moved. At v1.0.0 the called workflow declared its concurrency group as
pr-hygiene-<pull request number>, and this board's own
.github/workflows/pr-hygiene.yml declares that same string. Both set
cancel-in-progress, so whichever run was created second cancelled the other,
and the loser was the local gate.

$ grep -A2 '^concurrency:' .github/workflows/pr-hygiene.yml
concurrency:
  group: pr-hygiene-${{ github.event.pull_request.number }}
  cancel-in-progress: true

$ gh api "repos/iderex/wache/contents/.github/workflows/pr-hygiene.yml?ref=9b311243c2d0d0ced7feb957a20bc178acce6a5d" \
    --jq .content | base64 -d | grep -A2 '^concurrency:'
concurrency:
  group: pr-hygiene-${{ github.event.pull_request.number }}
  cancel-in-progress: true

Three placements of a group on the caller were tried and all three lost the
local gate:

  1. no group of my own, the file as the roll out prescribes it. Run 32611192849,
    cancelled, created 01:46:53, cancelled 01:46:54.
  2. a group of my own at workflow level, shared-hygiene-<number>. Run
    32611334638, cancelled, created 01:50:27, cancelled 01:50:28.
  3. the same group on the calling job rather than on the workflow. Run
    32611542281, cancelled, created 01:55:30.

Attempt 2 is the one that settles where the claim comes from. Had the caller's
own block decided the run's group, the shared call would have sat in
shared-hygiene-<number> and nothing would have been left in
pr-hygiene-<number> to cancel the local gate. It was cancelled anyway, so the
called workflow claims that group independently of its caller, and no caller
side declaration can move it.

The fix belonged in iderex/wache and was filed there as iderex/wache#4. It
has landed, and the group is namespaced:

$ gh issue view 4 --repo iderex/wache --json number,state,stateReason \
    --jq '"\(.number) \(.state) \(.stateReason)"'
4 CLOSED COMPLETED

$ gh api "repos/iderex/wache/contents/.github/workflows/pr-hygiene.yml?ref=113085b269d3437a3f96ff9e7060b64b0af88ab1" \
    --jq .content | base64 -d | grep -A2 '^concurrency:'
concurrency:
  group: wache-pr-hygiene-${{ github.event.pull_request.number }}
  cancel-in-progress: true

So the shared run now sits in wache-pr-hygiene-<number> and this board's gate
keeps pr-hygiene-<number>. The two no longer name one group, and both report a
verdict on this pull request, which is the measurement iderex/wache#4 named as
its own condition of done.

What made the collision cost a verdict rather than a red tick is local and has
not changed. A cancelled check blocks no merge here, because the ruleset on
main requires no status check:

$ gh api repos/Flowfin/core/rulesets/20572113 \
    --jq '{enforcement, bypass: .bypass_actors, required: [.rules[].type]}'
{"bypass":[],"enforcement":"active","required":["deletion","non_fast_forward","pull_request"]}

so a board's own rules can stop being judged while the tick beside them stays
green. #26 is where that is held open, and nothing here answers it.

What failure it prevents

A failure that has already happened rather than one that could. The count is in
iderex/operations#1556: nineteen boards carried nineteen implementations of the
same three or four rules, in four languages, no two alike, so a two line change
to a rule they share cost nineteen edits and failed in nineteen different ways.
A recent change across the fleet took six attempts, because each board refused
it for a different reason. Every board that calls the shared half instead of
copying it is one board that stops contributing a new way to fail.

It prevents nothing this board's own gate already prevents. That gate still
runs, still reads the body, and still compares the changed paths against the
scope the named issue declares.

Evidence

The change is one added file and no other line:

$ git diff --name-only main..HEAD
.github/workflows/shared-hygiene.yml
$ git diff --stat main..HEAD
 .github/workflows/shared-hygiene.yml | 52 ++++++++++++++++++++++++++++++++++++
 1 file changed, 52 insertions(+)

The pin is the commit the tag v1.2.0 points at, dereferenced rather than
trusted:

$ gh api repos/iderex/wache/git/ref/tags/v1.2.0 --jq '.object.type + " " + .object.sha'
tag ea26725f1acaa2616c9810b955230c40ba548e98
$ gh api repos/iderex/wache/git/tags/ea26725f1acaa2616c9810b955230c40ba548e98 --jq .object.sha
113085b269d3437a3f96ff9e7060b64b0af88ab1

The file parses to what its comment claims it says:

$ python -c "import yaml; d=yaml.safe_load(open('.github/workflows/shared-hygiene.yml')); ..."
keys: ['name', True, 'permissions', 'jobs']
permissions: {'contents': 'read'}
uses: iderex/wache/.github/workflows/pr-hygiene.yml@113085b269d3437a3f96ff9e7060b64b0af88ab1
with: {'subject_names_issue': False}

That the subject rule has to be off here was measured, not assumed, and the
measurement was re-run at this head rather than quoted from the earlier one. Not
one authored subject on this board is in the square brackets the fleet rule
wants, and the twelve most recent carry no reference in the subject at all,
because this board puts it in the body, which is what its own gate reads:

$ gh api "repos/Flowfin/core/commits?per_page=100" \
    --jq '.[] | select(.parents|length==1) | .commit.message | split("\n")[0]'
authored subjects:        52
carrying [#N]:             0
carrying (#N) or any #N:  38
of the last twenty:        8

What a guard here refuses, and the proof it bites

The guard this change adds to the board is the shared check itself, and what it
refuses is listed on #178: an empty body, a closing keyword buried in a
paragraph, a placeholder title, and, where the rule is on, a commit subject that
names no issue. Here the subject rule is off, so this call refuses the first
three and says in its own log that it read no commit subject.

I did not have to construct a violation to watch it bite, because the check
constructs its own on every run: nine fixtures, one line that passes and one
line that bites per rule, judged before the change is judged, and a failed
fixture makes the run refuse to judge at all. Read out of this pull request's
own run at this pin, rather than out of a run on the board the check lives on:

$ gh run view 32762644837 --repo Flowfin/core --log | grep -E 'fixture ok|subject rule'
fixture ok     an empty body is refused
fixture ok     one line of body is enough
fixture ok     a placeholder title is refused
fixture ok     a closing keyword mid-paragraph
fixture ok     square brackets count
fixture ok     another board counts
fixture ok     round brackets do not count
fixture ok     a bare number does not count
fixture ok     a colour value is not a mention
the subject rule is off for this repository, so no commit subject was read

Nine fixture lines and the line saying the subject rule is off, which is what
#178 asks to be visible in that log. The refusal direction is proved by the
fixtures rather than by a violation of mine: each of the four rules has a line
the check must refuse and a line it must pass, and fixture ok on the refusing
line is that refusal executed. What is not shown here is the check refusing THIS
pull request, because this pull request does not violate it, and the fixtures
are the reason that is not a gap.

Both gates reported on this head, which is the collision being gone rather than
being worked around. The listing is deduplicated on purpose: this board's own
gate also runs on edited, so every edit to this body adds another run of it
under the same name, and a raw paste stops reproducing the moment the body
carrying it is saved.

$ gh api repos/Flowfin/core/commits/5749d447156ff21cbe3fe6f2a7aa777d638fed76/check-runs \
    --jq '.check_runs[] | "\(.name)	\(.conclusion)"' | sort -u
Analyse the shell the gate runs (shellcheck)     success
Audit workflows (zizmor)        success
DCO sign-off    success
dependency-review       success
Deterministic PR-hygiene checks success
Documents name paths that resolve       success
External addresses in documents, reported and not gating success
hygiene / Deterministic PR hygiene      success
Reject Trojan Source Unicode    success
shellcheck      success
zizmor  success

The reading that does not move when a run is added is the count of everything
that concluded as something else:

$ gh api repos/Flowfin/core/commits/5749d447156ff21cbe3fe6f2a7aa777d638fed76/check-runs \
    --jq '[.check_runs[] | select(.conclusion != "success") | .name] | length'
0

Deterministic PR-hygiene checks is this board's own gate and it carries a
verdict rather than a cancellation. Every other check is the colour it was
before this branch existed.

The two files #178 says must not move did not:

$ git ls-tree origin/main -- .github/pr-hygiene/hygiene.sh .github/workflows/pr-hygiene.yml
100644 blob 7466cc9fd27de2d5f001e87ff384f28102666986    .github/pr-hygiene/hygiene.sh
100644 blob 2ac6b2125bd768d9f23c2edcde9fd22aa36e397a    .github/workflows/pr-hygiene.yml
$ git ls-tree HEAD -- .github/pr-hygiene/hygiene.sh .github/workflows/pr-hygiene.yml
100644 blob 7466cc9fd27de2d5f001e87ff384f28102666986    .github/pr-hygiene/hygiene.sh
100644 blob 2ac6b2125bd768d9f23c2edcde9fd22aa36e397a    .github/workflows/pr-hygiene.yml

Same blobs, so byte for byte rather than line for line.

No guard on this board is added, edited or deleted by this change, so there is
nothing here whose deletion could be shown to redden a suite.

What this does not cover

A green Shared hygiene here is not evidence that the shared rules are right,
only that they run across a repository boundary and that this board passes them.

It is no evidence at all about the subject rule, because that rule is off on
this call and no commit subject is read. The log line says so in as many words,
and a reader should take the green tick as covering three rules rather than
four.

It measures nothing about .github/pr-hygiene/hygiene.sh. The two run
independently and neither covers the other's gaps. The scope comparison, the
qualifier aware reference reading, and this board's stricter sense of an empty
body exist only in the local gate and are not touched, retested or replaced
here.

It is also not evidence that any local rule is now redundant. Nothing was
removed, and nothing should be until the shared check is shown, rule by rule, to
cover it.

The collision is settled at this pin and not in general. Nothing here stops a
later version of the called workflow from claiming the bare pattern again, and
nothing on this board reads the group a pin claims before the pin is moved. The
comment in the file names the string to check by hand, and that is a sentence
rather than a mechanism.

Nothing here was run on Windows or on any runner other than ubuntu-latest, and
nothing here reads the called workflow's behaviour on a fork pull request.

Who has read it

Nobody other than me. This is the third board of a fleet wide roll out and
follows iderex/bremsweg#143 and iderex/hoersaal#174, both merged, so the
shape has been through the checks on those boards rather than through a person.
That is not a second reader, and this pull request does not have one.

@iderex
iderex force-pushed the issue-178-shared-hygiene branch 3 times, most recently from 78f8da8 to cc02192 Compare August 23, 2026 01:57
One workflow file, which calls iderex/wache/.github/workflows/pr-hygiene.yml
with the subject rule off. .github/pr-hygiene/hygiene.sh is untouched and keeps
every rule it has, including the two the shared check has no equivalent for:
the scope comparison against the Scope: line a named issue declares, and the
qualifier aware reading that leaves another repository's issue number alone.

This does not merge as it stands and the file says why. The called workflow
declares pr-hygiene-<pull request number> as its concurrency group and this
board's own gate declares the same string, so the run created second cancels
the other. The local gate is the one that dies, one second after creation,
with no steps and no verdict. A group declared here does not help, at workflow
level or on the calling job: the called workflow claims the group
independently of the caller. Three attempts are on the pull request.

The reference is pinned by commit hash with the version beside it, because a
moving reference would let the called repository change what executes here
without anybody reviewing it.

Closes #178

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
At v1.0.0 the called workflow claimed `pr-hygiene-<pull request number>` as its
concurrency group, which is the string .github/workflows/pr-hygiene.yml on this
board already claims. Both cancel in progress, so the run created second
cancelled the other and the local gate was the one that died: cancelled one
second after creation, no steps, no verdict. This pull request has been open and
red in that state since it was created on 2026-08-23.

What was wrong was the pin rather than anything in this tree, and it was found by
re-reading the blocker it was waiting on rather than by a new run here.
iderex/wache#4 closed as completed and namespaced the group. At the commit pinned
now the shared run sits in `wache-pr-hygiene-<number>` while this board's gate
keeps `pr-hygiene-<number>`, so the two no longer meet:

    gh api repos/iderex/wache/git/ref/tags/v1.2.0 --jq '.object.sha'
    ea26725f1acaa2616c9810b955230c40ba548e98
    gh api repos/iderex/wache/git/tags/ea26725f1acaa2616c9810b955230c40ba548e98 --jq '.object.sha'
    113085b269d3437a3f96ff9e7060b64b0af88ab1

    gh api "repos/iderex/wache/contents/.github/workflows/pr-hygiene.yml?ref=113085b269d3437a3f96ff9e7060b64b0af88ab1" \
      --jq .content | base64 -d | grep -A2 '^concurrency:'
    concurrency:
      group: wache-pr-hygiene-${{ github.event.pull_request.number }}
      cancel-in-progress: true

The failure this prevents is a gate that reads as present and judges nothing. A
cancelled check blocks no merge here, because the ruleset on main requires no
status check:

    gh api repos/Flowfin/core/rulesets/20572113 \
      --jq '{enforcement, bypass: .bypass_actors, required: [.rules[].type]}'
    {"bypass":[],"enforcement":"active","required":["deletion","non_fast_forward","pull_request"]}

so this board's own rules stopped being judged while the tick beside them stayed
green, which is worse than not calling the shared check at all.

Nothing else changes. `.github/pr-hygiene/hygiene.sh` and
`.github/workflows/pr-hygiene.yml` are untouched, and the subject rule stays off.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit fdd19f9 into main Aug 24, 2026
14 checks passed
@iderex
iderex deleted the issue-178-shared-hygiene branch August 24, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Call the shared pull request hygiene check beside this board's own gate

1 participant